/**
 * Copyright © 2013 Creativor Studio.
 * About license information, please see LICENSE.txt.
 */
package org.creativor.rayson.share.transport.http;

import java.nio.ByteBuffer;

import org.creativor.rayson.share.annotation.NotNull;
import org.creativor.rayson.share.annotation.ThreadSafe;
import org.creativor.rayson.share.transport.BufferParser;
import org.creativor.rayson.share.transport.nio.ReadBuffer;
import org.creativor.rayson.util.LangUtil;

/**
 * An parser to parse HTTP protocol message. This parser should share a byte
 * buffer with an {@link ReadBuffer}.
 * 
 * @author creativor
 */
@ThreadSafe(false)
public abstract class HttpMessageParser<T extends AbstractHttpMessage>
		implements BufferParser {

	private static enum LastOnLight {
		NONE, ONE, TWO, THREE, FOUR
	}

	private static class Lights {
		private LastOnLight lastOnLight;

		Lights() {
			reset();
		}

		/**
		 * @return the lastOnLight
		 */
		public LastOnLight getLastOnLight() {
			return lastOnLight;
		}

		void reset() {
			lastOnLight = LastOnLight.NONE;
		}

		void testByte(final byte b) {
			switch (b) {

			case HttpConstants.CR: {
				switch (lastOnLight) {
				case NONE:
					lastOnLight = LastOnLight.ONE;
					break;
				case TWO:
					lastOnLight = LastOnLight.THREE;
					break;
				default:
					reset();
					break;
				}
				break;
			}

			case HttpConstants.LF: {
				switch (lastOnLight) {
				case ONE:
					lastOnLight = LastOnLight.TWO;
					break;
				case THREE:
					lastOnLight = LastOnLight.FOUR;
					break;

				default:
					reset();
					break;
				}
				break;
			}

			default: {
				if (lastOnLight != LastOnLight.NONE)
					reset();
				break;
			}
			}
		}
	}

	private enum State {
		NEW, START_LINE, HEADERS, BODY, READY;
	}

	/**
	 * Copy byte array from source byte array.
	 * 
	 * @param src
	 *            Source byte array.
	 * @param fromIndex
	 *            Copy from index.
	 * @param toIndex
	 *            Copy to byte index. Excluded.
	 * @return new copied byte array.
	 */
	protected static byte[] copy(final byte[] src, final int fromIndex,
			final int toIndex) {
		final byte[] result = new byte[toIndex - fromIndex];
		for (int i = 0; i < result.length; i++) {
			result[i] = src[fromIndex + i];
		}
		return result;
	}

	/**
	 * Search a key byte from source byte array. Begin from the start index.
	 * 
	 * @return First matched byte index.Or -1, if no key byte found.
	 */
	protected static int search(final byte[] src, final int startIndex,
			final byte key) {
		final int result = -1;
		for (int i = startIndex; i < src.length; i++) {
			if (src[i] == key)
				return i;

		}
		return result;
	}

	private final ByteBuffer buffer;
	private final Lights lights;

	private int readIndex;

	private ByteBuffer bodyBuffer;

	/**
	 * Parsed position in the underling byte buffer.
	 */
	private int parsedPos;

	private State state;

	private T lastMessage;

	protected HttpMessageParser(final ByteBuffer byteBuffer) {
		LangUtil.testNullArgument(byteBuffer, "Byte buffer");
		this.buffer = byteBuffer;
		lights = new Lights();
		readIndex = 0;
		state = State.NEW;
		parsedPos = 0;
	}

	@Override
	public void compactPositions() {
		this.readIndex = this.readIndex - this.parsedPos;
		this.parsedPos = 0;
	}

	@Override
	public boolean consume(final byte b) throws HttpProtocolException {

		switch (state) {
		case NEW: {
			lastMessage = newMessage();
			state = State.START_LINE;
			// goto start line state.
		}
		case START_LINE: {
			final LastOnLight lastOnLight = parse(b);
			if (lastOnLight == LastOnLight.TWO) {
				final byte[] newLine = readLine();
				lastMessage.setStartLine(parseStartLine(newLine));
				state = State.HEADERS;
			}
			// consumed.
			break;
		}
		case HEADERS: {
			final LastOnLight lastOnLight = parse(b);
			switch (lastOnLight) {
			case TWO: {
				final byte[] newLine = readLine();
				final HttpHeader header = parseHeader(newLine);
				lastMessage.getHeaders().add(header);
				state = State.HEADERS;
				break;
			}
			case FOUR: {
				readIndex += 2;
				final int bodyLength = lastMessage.getBodyLength();
				if (bodyLength == 0) {
					state = State.READY;
				} else {
					this.bodyBuffer = ByteBuffer.allocate(bodyLength);
					state = State.BODY;
				}
				lights.reset();
				break;
			}
			default:
				break;
			}

			// consumed.
			break;
		}
		case READY: {
			// Rejected.
			return false;
		}
		case BODY: {
			bodyBuffer.put(b);
			readIndex++;
			parsedPos++;
			if (!bodyBuffer.hasRemaining()) {
				lastMessage.setBody(bodyBuffer.array());
				this.bodyBuffer = null;
				state = State.READY;
			}
			break;
		}
		default:
			break;
		}

		return true;
	}

	/**
	 * @return True if new message is ready.
	 */
	public boolean isMessageReady() {
		return state == State.READY;
	}

	/**
	 * Create a new message and return.
	 */
	abstract T newMessage();

	private LastOnLight parse(final byte b) {
		lights.testByte(b);
		parsedPos++;
		return lights.getLastOnLight();
	}

	private HttpHeader parseHeader(@NotNull final byte[] line)
			throws HttpProtocolException {
		int pos = 0;
		final int nameIndex = search(line, pos,
				(byte) HttpConstants.MESSAGE_HEADER_SPLITER);
		if (nameIndex <= 0)
			throw new HttpProtocolException("Header line format is wrong");
		final String name = new String(copy(line, pos, nameIndex));

		pos = nameIndex + 2;
		if (pos >= line.length - 2)
			throw new HttpProtocolException("Header line format is wrong");
		final String value = new String(copy(line, pos, line.length - 2));
		return new HttpHeader(name, value);
	}

	/**
	 * Parse an {@link HttpStartLine} from a line byte array content.
	 * 
	 * @param line
	 * @return Parsed start line object.
	 * @throws HttpProtocolException
	 *             If the line format is wrong.
	 */
	abstract protected HttpStartLine parseStartLine(@NotNull final byte[] line)
			throws HttpProtocolException;

	/**
	 * @return
	 */
	private byte[] readLine() {
		final int lineLength = parsedPos - readIndex;
		final byte[] newLine = new byte[lineLength];
		for (int i = 0; i < newLine.length; i++) {
			newLine[i] = buffer.get(readIndex++);
		}
		return newLine;
	}

	/**
	 * Take the ready message.
	 * 
	 * @return Last ready message.
	 */
	public T takeMessage() {
		if (!isMessageReady())
			throw new IllegalStateException("Message is not ready yet");
		final T msg = this.lastMessage;
		state = State.NEW;
		this.lastMessage = null;
		return msg;
	}
}